{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Building a Simple Task Manager with llmio\n",
    "\n",
    "In this notebook, we'll build a Task Manager using `llmio`, a lightweight Python library designed to make working with large language models (LLMs) easier. This example demonstrates how `llmio` can be used to create a simple yet powerful application that can list, create, update, and delete tasks through natural language interactions.\n",
    "\n",
    "**Note**: This is a demo application where tasks are stored in memory. When you close the application, all tasks will be lost. This design allows us to focus on understanding `llmio`'s capabilities without worrying about persistent storage.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Define the Task model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "# Import necessary modules for defining data structures and type annotations.\n",
    "from dataclasses import dataclass\n",
    "from typing import Literal\n",
    "\n",
    "\n",
    "# Define the data model for a Task.\n",
    "# Each task has an ID, a name, a description, and a status (which can be \"todo\", \"doing\", or \"done\").\n",
    "@dataclass\n",
    "class Task:\n",
    "    id: int\n",
    "    name: str\n",
    "    description: str\n",
    "    status: Literal[\"todo\", \"doing\", \"done\"] = \"todo\"\n",
    "\n",
    "\n",
    "# (for demonstration purposes) Define a list of tasks\n",
    "TASKS: list[Task] = []"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Define the agent\n",
    "\n",
    "llmio Agents are used to define the behaviour and capabilities of the application.\n",
    "The Agent requires an instruction that informs the agent of its tasks and desired behaviour.\n",
    "\n",
    "The Agent class also requires a client to interact with the language model. Here, we use an OpenAIClient client.\n",
    "By default, this will talk to the OpenAI API, but other providers are also supported."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "\n",
    "import llmio\n",
    "\n",
    "\n",
    "# Create an Agent with specific instructions and a client for interacting with the OpenAI API.\n",
    "# The agent will act as a task manager, handling various task-related operations.\n",
    "agent = llmio.Agent(\n",
    "    instruction=\"You are a task manager.\",\n",
    "    client=llmio.OpenAIClient(api_key=os.environ[\"OPENAI_TOKEN\"]),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Define the agent's capabilities with tools\n",
    "\n",
    "Tools define the capabilities of the agents built with llmio. In this case, we will give the agent the capability to list, create, update, and delete tasks."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define a tool to list all tasks currently stored in the task manager.\n",
    "# This tool is registered with the agent using the @agent.tool decorator.\n",
    "@agent.tool\n",
    "def list_tasks():\n",
    "    print(\"** Listing tasks\")\n",
    "    return TASKS\n",
    "\n",
    "\n",
    "# Define a tool to add a task\n",
    "@agent.tool\n",
    "def add_task(name: str, description: str, status: Literal[\"todo\", \"doing\", \"done\"] = \"todo\"):\n",
    "    print(f\"** Adding task '{name}' with status '{status}'\")\n",
    "    task_id = len(TASKS) + 1\n",
    "    TASKS.append(Task(id=task_id, name=name, description=description, status=status))\n",
    "    return \"Created task with ID \" + str(task_id)\n",
    "\n",
    "\n",
    "# Define a tool to update an existing task.\n",
    "# This tool allows the agent to modify the status or description of a task based on its ID.\n",
    "@agent.tool\n",
    "def update_task(task_id: int, status: Literal[\"todo\", \"doing\", \"done\"] | None = None, description: str | None = None):\n",
    "    print(f\"** Updating task {task_id}\")\n",
    "    for task in TASKS:\n",
    "        if task.id == task_id:\n",
    "            task.status = status or task.status\n",
    "            task.description = description or task.description\n",
    "            return \"Updated task status on task \" + str(task_id)\n",
    "    return \"Task not found\"\n",
    "\n",
    "\n",
    "# Define a tool to remove a task\n",
    "@agent.tool\n",
    "def remove_task(task_id: int):\n",
    "    print(f\"** Removing task {task_id}\")\n",
    "    for task in TASKS[:]:\n",
    "        if task.id == task_id:\n",
    "            TASKS.remove(task)\n",
    "            return f\"OK - removed task {task_id}\"\n",
    "    return \"Task not found\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Interact with the agent\n",
    "\n",
    "The `Agent.speak` method is used to interact with the agent. This method sends a message to the language model and returns the response.\n",
    "If tool call requests are received, the agent will execute the appropriate tool method and return the result to the language model, continuing the iteration until only a message is returned.\n",
    "\n",
    "The `speak` method returns an `llmio.AgentResponse` object containing the received message from the language model, and conversation history so far.\n",
    "Since the agent is designed to be stateless, in order for parallel execution of messages / commands to be possible, the conversation history is used to maintain the state of the agent. We will therefore store the history and use it to continue the conversation in the later interactions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Bot: Hello! How can I assist you today?\n"
     ]
    }
   ],
   "source": [
    "# Start a conversation with the agent. The initial message initializes a message history \n",
    "# that we can use to keep track of the conversation across multiple interactions.\n",
    "response = await agent.speak(\"Hi!\")\n",
    "\n",
    "# Print the agent's responses to the console.\n",
    "for message in response.messages:\n",
    "    print(f\"** Bot: {message}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Listing tasks\n",
      "** Bot: You currently have no tasks listed. Would you like to add a new task?\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = await agent.speak(\"List my tasks\", history=response.history)\n",
    "\n",
    "for message in response.messages:\n",
    "    print(f\"** Bot: {message}\")\n",
    "\n",
    "TASKS"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Add a message handler\n",
    "\n",
    "Instead of waiting for messages to return, we can add a message handler to process agent messages as they come in.\n",
    "\n",
    "This can be used to call a webhook or to process the message in some other way.\n",
    "\n",
    "In this example, we will use a message handler to print the agent's response."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Define a message handler to automatically process and print messages as they come in.\n",
    "# This can be extended to trigger other actions, like sending notifications or updating a UI.\n",
    "@agent.on_message\n",
    "async def handle_message(message: str) -> None:\n",
    "    print(f\"** Bot (message_handler): '{message}'\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Continue the interaction, but using the message handler to print the messages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Adding task 'Grocery Shopping' with status 'todo'\n",
      "** Bot (message_handler): 'I've added a grocery task for you to buy eggs, bacon, bread, tomatoes, and orange juice. If there's anything else you'd like to do, just let me know!'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[Task(id=1, name='Grocery Shopping', description='Buy eggs, bacon, bread, tomatoes, and orange juice.', status='todo')]"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = await agent.speak(\"Add a grocery task with a description to buy eggs, bacon, bread, tomatoes and orange juice\", history=response.history)\n",
    "\n",
    "TASKS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Adding task 'Clean the House' with status 'todo'\n",
      "** Bot (message_handler): 'I've added a task to clean the house. If you need anything else or want to manage your tasks further, feel free to ask!'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[Task(id=1, name='Grocery Shopping', description='Buy eggs, bacon, bread, tomatoes, and orange juice.', status='todo'),\n",
       " Task(id=2, name='Clean the House', description='Thoroughly clean all rooms and tidy up.', status='todo')]"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = await agent.speak(\"Also add a task to clean the house\", history=response.history)\n",
    "\n",
    "TASKS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Updating task 1\n",
      "** Bot (message_handler): 'I've updated the grocery task to include milk along with eggs, bacon, bread, tomatoes, and orange juice. If you need any more changes or additional tasks, just let me know!'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[Task(id=1, name='Grocery Shopping', description='Buy eggs, bacon, bread, tomatoes, orange juice, and milk.', status='todo'),\n",
       " Task(id=2, name='Clean the House', description='Thoroughly clean all rooms and tidy up.', status='todo')]"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = await agent.speak(\"i also need to buy some milk, can you add that to the grocery list?\", history=response.history)\n",
    "\n",
    "TASKS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Updating task 1\n",
      "** Bot (message_handler): 'I've marked the grocery task as done. If you have any more tasks to manage or need assistance with anything else, just let me know!'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[Task(id=1, name='Grocery Shopping', description='Buy eggs, bacon, bread, tomatoes, orange juice, and milk.', status='done'),\n",
       " Task(id=2, name='Clean the House', description='Thoroughly clean all rooms and tidy up.', status='todo')]"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = await agent.speak(\"The grocery task is done\", history=response.history)\n",
    "\n",
    "TASKS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Removing task 1\n",
      "** Bot (message_handler): 'I've removed the completed grocery task. If you need further assistance or have more tasks to manage, feel free to let me know!'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[Task(id=2, name='Clean the House', description='Thoroughly clean all rooms and tidy up.', status='todo')]"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = await agent.speak(\"remove all completed tasks\", history=response.history)\n",
    "\n",
    "TASKS"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "** Listing tasks\n",
      "** Bot (message_handler): 'You currently have one task:\n",
      "\n",
      "1. **Clean the House**: Thoroughly clean all rooms and tidy up. (Status: Todo)\n",
      "\n",
      "If you need any further assistance with this task or others, just let me know!'\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[Task(id=2, name='Clean the House', description='Thoroughly clean all rooms and tidy up.', status='todo')]"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = await agent.speak(\"can you list my current tasks?\", history=response.history)\n",
    "\n",
    "TASKS"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Conclusion\n",
    "\n",
    "In this notebook, we've built a simple task manager using `llmio` and demonstrated how to interact with it using natural language commands. This example showcases how `llmio` enables you to easily define, manage, and extend the capabilities of language model agents.\n",
    "\n",
    "For further exploration, consider extending this task manager by:\n",
    "- Adding persistent storage using a database.\n",
    "- Integrating with a web UI for managing tasks visually.\n",
    "- Expanding the agent's capabilities to include more complex operations or integrations.\n",
    "\n",
    "Feel free to experiment with `llmio` and see how it can be adapted to your projects. Happy coding!"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
